/*   Copyright (C) 2008  Jeroen v. Schagen & Youri Lima
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;

using Engine.Math;

namespace Engine.Rendering
{
    /// <summary>
    ///     The view frustum is the volume that contains everything that 
    ///     is potentially (there may be occlusions) visible on the screen. 
    /// 
    ///     This volume is defined according to the camera's settings, and when 
    ///     using a perspective projection takes the shape of a truncated pyramid.
    /// </summary>
    public class Frustum
    {
        protected Camera camera;

        protected float angle;
        protected float ratio;
        protected float near;
        protected float far;

        protected Plane[] plane;

        # region Constructor

        /// <summary>
        ///     Construct frustum.
        /// </summary>
        /// 
        /// <param name="obj">Origin</param>
        /// <param name="angle">Field of view</param>
        /// <param name="ratio">Ratio</param>
        /// <param name="near">Near clipping distance</param>
        /// <param name="far">Far clipping distance</param>
        public Frustum(Camera camera, float angle, float ratio, float near, float far)
        {
            this.camera = camera;

            this.angle = angle;
            this.ratio = ratio;
            this.near = near;
            this.far = far;

            plane = new Plane[6];

            // Build frustum
            Build();
        }

        # endregion


        # region Function

        /// <summary>
        ///     Build the frustum pyramid, using the object as origin.
        /// </summary>
        /// 
        /// <param name="obj">Origin</param>
        public void Build()
        {
            // Measure near clipping plane
            float hnear = (float) (2.0 * System.Math.Tan(angle / 2) * near);
            float wnear = hnear * ratio;

            // Measure far clipping plane
            float hfar = (float) (2.0 * System.Math.Tan(angle / 2) * far);
            float wfar = hfar * ratio;

            // Determine plane points
            Vector3 cfar = camera.WorldPosition + camera.Direction * far;
            Vector3 cnear = camera.WorldPosition + camera.Direction * near;

            Vector3 ftl = cfar + (Vector3.UNIT_Y * hfar / 2) - (Vector3.UNIT_X * wfar / 2);
            Vector3 ftr = cfar + (Vector3.UNIT_Y * hfar / 2) + (Vector3.UNIT_X * wfar / 2);
            Vector3 fbl = cfar - (Vector3.UNIT_Y * hfar / 2) - (Vector3.UNIT_X * wfar / 2);
            Vector3 fbr = cfar - (Vector3.UNIT_Y * hfar / 2) + (Vector3.UNIT_X * wfar / 2);

            Vector3 ntl = cnear + (Vector3.UNIT_Y * hnear / 2) - (Vector3.UNIT_X * wnear / 2);
            Vector3 ntr = cnear + (Vector3.UNIT_Y * hnear / 2) + (Vector3.UNIT_X * wnear / 2);
            Vector3 nbl = cnear - (Vector3.UNIT_Y * hnear / 2) - (Vector3.UNIT_X * wnear / 2);
            Vector3 nbr = cnear - (Vector3.UNIT_Y * hnear / 2) + (Vector3.UNIT_X * wnear / 2);

            // Initiate planes
            plane[0] = new Plane(ftr, ftl, fbl);
            plane[1] = new Plane(ntl, ntr, nbr);
            plane[2] = new Plane(ntl, nbl, fbl);
            plane[3] = new Plane(nbr, ntr, fbr);
            plane[4] = new Plane(ntr, ntl, ftl);
            plane[5] = new Plane(nbl, nbr, fbr);
        }

        /// <summary>
        ///     Check if a certain point is visible inside this frustum.
        /// </summary>
        /// 
        /// <param name="p">Point</param>
        /// <returns>Visible</returns>
        public bool InFrustum(Vector3 p)
        {
            for (int i = 0; i < 6; i++)
            {
                if (plane[i].Distance(p) < 0)
                {
                    return false;
                }
            }

            return true;
        }

        # endregion


        # region Accessor

        public float Angle
        {
            get { return angle; }

            set
            {
                this.angle = value;

                // Rebuild frustum
                Build();
            }
        }

        public float Ratio
        {
            get { return ratio; }

            set
            {
                this.ratio = value;

                // Rebuild frustum
                Build();
            }
        }

        public float Near
        {
            get { return near; }

            set
            {
                this.near = value;

                // Rebuild frustum
                Build();
            }
        }

        public float Far
        {
            get { return far; }

            set
            {
                this.far = value;

                // Rebuild frustum
                Build();
            }
        }

        public Plane[] Plane
        {
            get { return plane; }
        }

        # endregion
    }
}
